﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.IO;
using Microsoft.Win32;
using System.Windows.Media.Animation;
using System.Windows.Threading;

namespace ACPLogAnalyzer
{
    /// <summary>
    /// The MainWindow class runs the UI and starts news threads to carry out tasks like enumerating a directory
    /// structure to find log files and parsing one or more logs
    /// </summary>
    public partial class MainWindow : Window
    {
        #region Member variables and Properties
        private bool m_ReadingLogs = false;                                 // Flag used to signal to the "get logs" thread that it should stop
        private bool m_plottingLogs = false;                                // Flag used to signal if we're plotting a graph
        private bool m_forceUpdateReport;                                   // Flag used to force the showing of the latest version dlg
        private bool m_configRestart = false;                               // Hack for restarting the graph config to force line colors to change
        private int m_nonACPLogFileCount;                                   // Keeps track of non-ACP logs the user attempted to add
        private int m_configRestartTab = 0;                                 // Hack for restarting the graph config dialog
        private string m_graphTitle;                                        // The title of the graph to create
        private delegate void ReadLogsInPath(string path);                  // Delegate used to run the "get logs" thread
        private delegate void ParseLogs(string path, bool forGraph);        // Delegate used to run the "parse logs" thread
        private delegate void CheckForUpdate();                             // Delegate used to run the thread which sees if any update is available
        private ReadLogsInPath m_readLogsOp = null;                         // Instance of the "get logs" delegate
        private ParseLogs m_parseLogsOp = null;                             // Instance of the "parse logs" delegate
        private CheckForUpdate m_checkForUpdateOp = null;                   // Instance of the delegate
        private AppVersionChecker m_appVersionChecker = null;               // Object checks if an update is available from the web
        private AppSettings m_appSettings = null;                           // Object that deals with reading/writing registry settings
        private LogReader m_logReader = null;                               // Object that deals with reading a log (or any text) from disk
        private Report m_report = null;
        private List<KeyValuePair<object, object>> m_grDataSeries1;         // Holds data for Data V Time type graphs
        private List<KeyValuePair<object, object>> m_grDataSeries2;         // Holds data for Data V Time type graphs
        private List<KeyValuePair<object, object>> m_grDataSeries3;         // Holds data for Data V Time type graphs
        private List<KeyValuePair<GraphType, string>> m_graphOptions;       // Holds the list of graph types
        private GraphType m_graphType;                                      // Holds the type of graph to create
        
        private bool m_parsingLogs = false;                                 // Flag used to signal to the "parse all logs" thread
        public bool ParsingLogs
        {
            get { return m_parsingLogs; }
            set 
            { 
                m_parsingLogs = value;
                if (m_report != null)
                    m_report.ParsingLogs = value;
            }
        }
        #endregion

        #region Initialization
        public MainWindow()
        {
            InitializeComponent();

            m_forceUpdateReport = false;
            m_graphType = GraphType.NoSelection;

            // This allows graph child windows to be automatically closed when the main window closes
            Application.Current.MainWindow = this;
            Application.Current.ShutdownMode = ShutdownMode.OnMainWindowClose;

            InitPaths();
            InitGraphs();
        }

        private void InitPaths()
        {
            // Read ACP-related info from the registry (e.g. likely places for logs)
            m_appSettings = new AppSettings();
            m_appSettings.ReadRegistry();

            if (!string.IsNullOrEmpty(m_appSettings.DocRoot))
                comboBoxPath.Items.Add(m_appSettings.DocRoot);

            if (!string.IsNullOrEmpty(m_appSettings.LogDir))
            {
                comboBoxPath.Items.Add(m_appSettings.LogDir);
                comboBoxPath.SelectedValue = m_appSettings.LogDir;
            }

            // Do we have a most recently used search dir?
            if (!string.IsNullOrEmpty(Properties.Settings.Default.Most_Recent_Search_Dir))
            {
                if (!IsInList(comboBoxPath.Items, Properties.Settings.Default.Most_Recent_Search_Dir))
                    comboBoxPath.Items.Insert(0, Properties.Settings.Default.Most_Recent_Search_Dir);
                comboBoxPath.SelectedIndex = 0;
            }
        }

        private void InitGraphs()
        {
            // Init the combo that holds a list of possible graph types...
            m_graphOptions = new List<KeyValuePair<GraphType, string>>();
            m_graphOptions.Add(new KeyValuePair<GraphType, string>(GraphType.NoSelection, "Graphs for the selected log:"));
            m_graphOptions.Add(new KeyValuePair<GraphType, string>(GraphType.FWHM, "  FWHM measurements"));
            m_graphOptions.Add(new KeyValuePair<GraphType, string>(GraphType.HFD, "  HFD measurements"));
            m_graphOptions.Add(new KeyValuePair<GraphType, string>(GraphType.FWHMvHFD, "  FWHM verses HFD"));
            m_graphOptions.Add(new KeyValuePair<GraphType, string>(GraphType.PointErrorObj, "  Pointing errors (object slew)"));
            m_graphOptions.Add(new KeyValuePair<GraphType, string>(GraphType.PointErrorCntr, "  Pointing errors (center slew)"));
            m_graphOptions.Add(new KeyValuePair<GraphType, string>(GraphType.GuiderStartUpTime, "  Guider start-up times"));
            m_graphOptions.Add(new KeyValuePair<GraphType, string>(GraphType.GuiderSettleTime, "  Guider settle times"));
            m_graphOptions.Add(new KeyValuePair<GraphType, string>(GraphType.FilterChangeTime, "  Filter change times"));
            m_graphOptions.Add(new KeyValuePair<GraphType, string>(GraphType.PtExpSolveTime, "  Pointing exp/plate solve times"));
            m_graphOptions.Add(new KeyValuePair<GraphType, string>(GraphType.SlewTimeTgts, "  Slew times (targets)"));
            m_graphOptions.Add(new KeyValuePair<GraphType, string>(GraphType.AFTimes, "  Auto-focus times"));
            m_graphOptions.Add(new KeyValuePair<GraphType, string>(GraphType.NoSelection, ""));
            m_graphOptions.Add(new KeyValuePair<GraphType, string>(GraphType.NoSelection, "Graphs for all logs:"));
            m_graphOptions.Add(new KeyValuePair<GraphType, string>(GraphType.FWHMAvgAllLogs, "  Avg FWHM measurements"));
            m_graphOptions.Add(new KeyValuePair<GraphType, string>(GraphType.HFDAvgAllLogs, "  Avg HFD measurements"));
            m_graphOptions.Add(new KeyValuePair<GraphType, string>(GraphType.FWHMvHFDAllLogs, "  Avg FWHM verses Avg HFD"));
            m_graphOptions.Add(new KeyValuePair<GraphType, string>(GraphType.PointErrorObjAllLogs, "  Avg pointing error (object slew)"));
            m_graphOptions.Add(new KeyValuePair<GraphType, string>(GraphType.PointErrorCntrAllLogs, "  Avg pointing errors (center slew)"));
            m_graphOptions.Add(new KeyValuePair<GraphType, string>(GraphType.GuiderStartUpTimeAllLogs, "  Avg guider start-up times"));
            m_graphOptions.Add(new KeyValuePair<GraphType, string>(GraphType.GuiderSettleTimeLogAllLogs, "  Avg guider settle times"));
            m_graphOptions.Add(new KeyValuePair<GraphType, string>(GraphType.FilterChangeTimeAllLogs, "  Avg filter change times"));
            m_graphOptions.Add(new KeyValuePair<GraphType, string>(GraphType.PtExpSolveTimeAllLogs, "  Avg pointing exp/plate solve times"));
            m_graphOptions.Add(new KeyValuePair<GraphType, string>(GraphType.SlewTimeTgtsAllLogs, "  Avg slew times (targets)"));
            m_graphOptions.Add(new KeyValuePair<GraphType, string>(GraphType.AFTimesAllLogs, "  Avg auto-focus times"));

            comboBoxGraphs.ItemsSource = m_graphOptions;
            comboBoxGraphs.DisplayMemberPath = "Value";
            comboBoxGraphs.SelectedValuePath = "Key";
            comboBoxGraphs.SelectedIndex = 1;
        }
        #endregion

        #region UI Event Handlers
        private void buttonReadLogs_Click(object sender, RoutedEventArgs e)
        {
            if (m_ReadingLogs)
            {
                // Stop the reading process
                ResetUI();
                return;
            }

            buttonReadLogs.Content = "Stop";
            m_ReadingLogs = true;

            string path = "";
            if (comboBoxPath.SelectedValue == null)
            {
                if (string.IsNullOrEmpty(comboBoxPath.Text))
                {
                    ResetUI();
                    return;
                }
                else
                    path = comboBoxPath.Text;
            }
            else
            {
                if (string.IsNullOrEmpty(comboBoxPath.SelectedValue.ToString()))
                {
                    ResetUI();
                    return;
                }
                else
                {
                    path = comboBoxPath.SelectedValue.ToString();
                    if (!comboBoxPath.Items.Contains(path))
                        comboBoxPath.Items.Add(path);
                }
            }

            m_nonACPLogFileCount = 0;
            m_readLogsOp = new ReadLogsInPath(ReadLogFile);
            m_readLogsOp.BeginInvoke(path, ReadLogsComplete, null);
        }

        private void buttonPath_Click(object sender, RoutedEventArgs e)
        {
            using (var fbd = new System.Windows.Forms.FolderBrowserDialog())
            {

                if (comboBoxPath.Items.Count > 0 &&
                    comboBoxPath.SelectedIndex != -1 &&
                    System.IO.Directory.Exists(comboBoxPath.SelectedValue.ToString()))
                    fbd.SelectedPath = comboBoxPath.SelectedValue.ToString();

                System.Windows.Forms.DialogResult result = fbd.ShowDialog();
                if (result == System.Windows.Forms.DialogResult.OK)
                {
                    comboBoxPath.Items.Add(fbd.SelectedPath);
                    comboBoxPath.SelectedValue = fbd.SelectedPath;

                    Properties.Settings.Default.Most_Recent_Search_Dir = fbd.SelectedPath;
                    Properties.Settings.Default.Save();
                }
            }
        }

        private void buttonLog_Click(object sender, RoutedEventArgs e)
        {
            OpenFileDialog ofd = new OpenFileDialog();

            if(comboBoxPath.Items.Count > 0 && System.IO.Directory.Exists(comboBoxPath.SelectedValue.ToString()))
                ofd.InitialDirectory = comboBoxPath.SelectedValue.ToString();

            ofd.Multiselect = false;
            ofd.DefaultExt = ".log"; // Default file extension
            ofd.Filter = "LOG|*.log"; // Filter files by extension
            Nullable<bool> result = ofd.ShowDialog();
            if (result == false)
                return;

            // Is this an ACP log file?
            if (!CheckLogIsACPLog(ofd.FileName))
            {
                MessageBox.Show("The selected log is not a valid ACP log", "Log Not Added");
                return;
            }

            if (!IsInList(listBoxLogs.Items, ofd.FileName))
            {
                listBoxLogs.Items.Add(ofd.FileName);
                UpdateLogListCount(listBoxLogs.Items.Count);
            }
            else
            {
                MessageBox.Show("The selected log is already in the list", "Log Not Added");
            }
        }

        private void listBoxLogs_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            if (listBoxLogs.SelectedIndex != -1)
                GenerateReport(listBoxLogs.SelectedValue.ToString());
        }

        private void buttonReportAll_Click(object sender, RoutedEventArgs e)
        {
            if (listBoxLogs.Items.Count == 0)
            {
                MessageBox.Show("Before generating a report you must add one or more ACP logs to the list.\nYou may either:\n\n1. Click the 'Add All Logs In Path' button to automatically add all logs found\n    in the directory structure indicated by the Path parameter\n2. Add a single log file using the 'Add Single Log' button\n3. Drag files into the logs list using Windows Explorer", "Unable to generate report");
                return;
            }

            GenerateReport(null);
        }

        private void buttonClearLogList_Click(object sender, RoutedEventArgs e)
        {
            listBoxLogs.Items.Clear();
            UpdateLogListCount(listBoxLogs.Items.Count);
        }

        private void MenuItemRemoveLog_Click(object sender, RoutedEventArgs e)
        {
            if (listBoxLogs.Items.Count > 0 && listBoxLogs.SelectedIndex != -1)
            {
                listBoxLogs.Items.RemoveAt(listBoxLogs.SelectedIndex);
                listBoxLog.Items.Clear();

                if (listBoxLogs.Items.Count > 0)
                    listBoxLogs.SelectedIndex = 0;

                UpdateLogListCount(listBoxLogs.Items.Count);
            }
        }

        private void buttonConfigReport_Click(object sender, RoutedEventArgs e)
        {
            ConfigReport cfgWnd = new ConfigReport();
            cfgWnd.ShowDialog();
        }

        private void buttonSaveReport_Click(object sender, RoutedEventArgs e)
        {
            if (listBoxReport.Items.Count == 0)
                return;

            try
            {
                string timestamp = DateTime.Now.ToLongTimeString();
                timestamp = timestamp.Replace(":", "");

                string path = Environment.GetFolderPath(Environment.SpecialFolder.Desktop) +
                    @"\ACPLogAnalyzer-Report-" + timestamp + ".txt";

                using (StreamWriter outfile = new StreamWriter(path))
                {
                    foreach (string line in listBoxReport.Items)
                        outfile.WriteLine(line);
                }

                MessageBox.Show("Report saved on your Desktop as:\n\n" + "ACPLogAnalyzer-Report-" + timestamp + ".txt", "Log Report Saved");
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message, "Error Saving Report");
            }
        }

        private void listBoxLogs_DragEnter(object sender, DragEventArgs e)
        {
            if (e.Data != null && ((DataObject)e.Data).ContainsFileDropList())
            {
                e.Effects = DragDropEffects.Copy;
            }
        }

        private void listBoxLogs_Drop(object sender, DragEventArgs e)
        {
            m_nonACPLogFileCount = 0;
            if (e.Data is DataObject && ((DataObject)e.Data).ContainsFileDropList())
            {
                foreach (string file in ((DataObject)e.Data).GetFileDropList())
                {
                    // Is this a file or a path?
                    if (System.IO.Directory.Exists(file))
                    {
                        // It's a directory - scan the dir structure for logs (and add the path to the Path combo)
                        comboBoxPath.Items.Add(file);
                        comboBoxPath.SelectedValue = file;
                        buttonReadLogs_Click(this, null);
                    }
                    else if (file.EndsWith(".log"))  // It's a file - make sure it has the right extn
                    {
                        // Add the log if it hasn't already been added
                        if (!IsInList(listBoxLogs.Items, file))
                        {
                            if (CheckLogIsACPLog(file))
                            {
                                listBoxLogs.Items.Add(file);
                                UpdateLogListCount(listBoxLogs.Items.Count);
                            }
                            else
                            {
                                // Silently reject non-ACP logs until the end of the add process
                                m_nonACPLogFileCount++;
                            }
                        }
                    }
                }

                if (m_nonACPLogFileCount > 0)
                    MessageBox.Show(m_nonACPLogFileCount.ToString() + 
                        " files were not added because they are not valid ACP logs", "File(s) not added");
            }
        }

        private void buttonHelp_Click(object sender, RoutedEventArgs e)
        {
            Help hlp = new Help();
            hlp.ShowDialog();
        }

        private void buttonViewNotepad_Click(object sender, RoutedEventArgs e)
        {
            // Open the selected log in the default text editor (normally Notepad.exe)
            if(listBoxLogs.Items.Count > 0 && listBoxLogs.SelectedIndex != -1)
                System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(listBoxLogs.SelectedValue.ToString()));
        }

        private void buttonHelpBrowser_Click(object sender, RoutedEventArgs e)
        {
            var uri = new Uri(Properties.Settings.Default.Help_Text_Url);
            System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(uri.AbsoluteUri));
        }

        private void listBoxLog_MouseDoubleClick(object sender, MouseButtonEventArgs e)
        {
            buttonViewNotepad_Click(this, null);
        }

        private void listBoxLogs_MouseDoubleClick(object sender, MouseButtonEventArgs e)
        {
            buttonViewNotepad_Click(this, null);
        }

        private void listBoxReport_MouseDoubleClick(object sender, MouseButtonEventArgs e)
        {
            // Save the report to a temporary file and then view in Notepad
            if (listBoxReport.Items.Count == 0)
                return;

            try
            {
                string timestamp = DateTime.Now.ToLongTimeString();
                timestamp = timestamp.Replace(":", "");

                string path = Environment.GetFolderPath(Environment.SpecialFolder.Windows) + @"\Temp\" +
                    @"\ACPLogAnalyzer-Report-" + timestamp + ".txt";

                using (StreamWriter outfile = new StreamWriter(path))
                {
                    foreach (string line in listBoxReport.Items)
                        outfile.WriteLine(line);
                }

                // Open in Notepad
                System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(path));
            }
            catch 
            {
                // Deliberate ignore - the double-click to view the report in notepad is 'hidden' feature, 
                // so we keep quiet if it fails
            }
        }

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            this.Title = GetMainWndTitle();

            // Kick-off a background thread to see if an update is available...
            if (Properties.Settings.Default.Always_Check_For_Updates)
            {
                m_checkForUpdateOp = new CheckForUpdate(DoCheckForUpdate);
                m_checkForUpdateOp.BeginInvoke(CheckForUpdateComplete, null);
            }
        }

        private void buttonPlot_Click(object sender, RoutedEventArgs e)
        {
            if (m_plottingLogs)
                return;

            if (comboBoxGraphs.SelectedValue == null)
                return;

            string log = null;
            m_plottingLogs = true; 
            
            if (listBoxLogs.SelectedIndex != -1)
                log = listBoxLogs.SelectedValue.ToString();  // The selected log (which may or may not be used, depending on the graph type selected)

            KeyValuePair<GraphType, string> selectedKvp = (KeyValuePair<GraphType, string>)comboBoxGraphs.SelectedItem;
            ParsingLogs = true;
            m_graphType = selectedKvp.Key;
            m_graphTitle = selectedKvp.Value;
            PreparePlot(selectedKvp.Key, selectedKvp.Value, log);
        }

        private void buttonUpdate_Click(object sender, RoutedEventArgs e)
        {
            if (m_appVersionChecker != null)
                return;  // Already checking 

            // Kick-off a background thread to see if an update is available...
            m_forceUpdateReport = true;
            m_checkForUpdateOp = new CheckForUpdate(DoCheckForUpdate);
            m_checkForUpdateOp.BeginInvoke(CheckForUpdateComplete, null);
        }

        private void buttonConfigPlot_Click(object sender, RoutedEventArgs e)
        {
            ConfigPlot cfgWnd = new ConfigPlot();
            cfgWnd.m_restartConfig = m_configRestart;
            cfgWnd.m_restartTab = m_configRestartTab;
            cfgWnd.ShowDialog();
            if (cfgWnd.m_restartConfig)
            {
                m_configRestart = true;
                m_configRestartTab = cfgWnd.m_restartTab;
                buttonConfigPlot_Click(this, null);
            }
            else
            {
                m_configRestart = false;
                m_configRestartTab = 0;
            }
        }
        #endregion

        #region Misc methods
        private void ResetUI()
        {
            buttonReadLogs.Content = "Add All Logs In Path";
            m_ReadingLogs = false;
        }

        private void ResetParsingAllUI()
        {
            buttonReportAll.Content = "Generate Report On All Logs";
            buttonPlot.IsEnabled = true;
            ParsingLogs = false;
        }

        private bool IsInList(ItemCollection list, string searchItem)
        {
            // This method is necessary as the standard Contains() method on the listbox doesn't work reliably for some reason
            foreach (string s in list)
            {
                if (s.CompareTo(searchItem) == 0)
                    return true;
            }
            return false;
        }

        private void DoCheckForUpdate()
        {
            m_appVersionChecker = new AppVersionChecker();
            m_appVersionChecker.CheckForUpdate();
        }

        private void CheckForUpdateComplete(IAsyncResult result)
        {
            this.Dispatcher.Invoke(System.Windows.Threading.DispatcherPriority.Normal, (Action)(() =>
            {
                if (m_appVersionChecker.IsUpdateAvailable || m_forceUpdateReport)
                {
                    Update updateWnd = new Update(m_appVersionChecker);
                    updateWnd.ShowDialog();
                }
                else
                {
                    // Remove any previous (old) installer if present...
                    m_appVersionChecker.RemoveInstaller();
                }

                m_appVersionChecker = null;
                m_forceUpdateReport = false;
            }));
        }

        private void UpdateLogListCount(int count)
        {
            groupBoxLogsList.Header = "Logs (" + count.ToString() + ") (drag log files into this list)";
        }

        public void ScrollLogToTime(KeyValuePair<object, object> data, LogEventType logEventType, int lineNumber)
        {
            if (lineNumber == -1)
                return;

            try
            {
                listBoxLog.ScrollIntoView(listBoxLog.Items[lineNumber]);
                listBoxLog.SelectedIndex = lineNumber;
            }
            catch
            {
            }
        }

        private string GetMainWndTitle()
        {
            StringBuilder sb = new StringBuilder();

            sb.Append(
                "ACP Log Analyzer - Version " +
                System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.Major.ToString() +
                "." +
                System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.Minor.ToString() +
                System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.Build.ToString());

            return sb.ToString();
        }
        #endregion

        #region Log reading/parsing methods
        /// <summary>
        /// Kick-off the report generating thread. This method can be used to generate for either all reports in 
        /// listBoxLogs (the log param should be set to null or an empty string) or for a particular. In the latter case
        /// the log param should be the log selected in listBoxLogs
        /// </summary>
        /// <param name="log">The path to the log to generate a report on. Alternatively, passing null or an empty
        /// string causes a report to be generated for all items in listBoxLogs
        /// </param>
        private void GenerateReport(string log)
        {
            if (ParsingLogs)
            {
                // Stop the parsing process
                ResetParsingAllUI();
                return;
            }

            buttonReportAll.Content = "Stop";
            buttonPlot.IsEnabled = false;
            ParsingLogs = true;

            // Kick-off the parsing process on a new thread...
            m_parseLogsOp = new ParseLogs(DoParseLogs);
            m_parseLogsOp.BeginInvoke(log, false, ParseLogsComplete, null);
        }

        /// <summary>
        /// Either parses all logs in listBoxLogs or, if the path param is non-null parses the indicated log 
        /// </summary>
        /// <param name="path">Either null/empty string (parse all logs) or the path to a specific log</param>
        private void DoParseLogs(string path, bool forGraph)
        {
            m_report = new Report(listBoxLogs.Items.Cast<String>().ToList());
            m_report.ParsingLogs = true;

            // Create the report for the log(s) and save to disk. This process will enumerate all the required logs, 
            // compile the stats, and output the report to a temporary text file
            string reportName = m_report.CreateReport(path, forGraph);
            if (forGraph)
                return;  // No further text processing required - the data is now ready for graphing

            if (string.IsNullOrEmpty(reportName))
            {
                ReportError("Error creating report");
                return;
            }

            if(!m_logReader.ReadLogFile(reportName))
            {
                ReportError("Error reading report from temporary file " + reportName);
                return;
            } 

            this.Dispatcher.Invoke(System.Windows.Threading.DispatcherPriority.Normal, (Action)(() =>
            {
                listBoxReport.Items.Clear();
                foreach (string line in m_logReader.LogFileText)
                    listBoxReport.Items.Add(line);

                if (string.IsNullOrEmpty(path))
                    return;  // The report was on all logs - leave the current log selected/displayed

                if (!m_logReader.ReadLogFile(path))
                {
                    ReportError("Error reading log contents: " + path);
                    return;
                }

                listBoxLog.Items.Clear();
                foreach (string line in m_logReader.LogFileText)
                    listBoxLog.Items.Add(line);
            }));
        }

        /// <summary>
        /// Displays an error message box
        /// </summary>
        /// <param name="msg"></param>
        private void ReportError(string msg)
        {
            this.Dispatcher.Invoke(System.Windows.Threading.DispatcherPriority.Normal, (Action)(() =>
            {
                MessageBox.Show(msg, "Report Error");
            }));
        }

        /// <summary>
        /// Parse to see if the log is a valid ACP log
        /// </summary>
        /// <param name="path"></param>
        private bool CheckLogIsACPLog(string path)
        {
            try
            {
                Log log = null;
                m_logReader = new LogReader(path);
                if (!m_logReader.ReadLogFile())
                    return false;

                // Create a new Log object and give it the text of its underlying log file
                log = new Log(path, m_logReader.LogFileText);

                // Is it an ACP log?
                if (log.ParseValidACPLog())
                    return true;
            }
            catch
            {
            }

            return false;
        }

        private void ReadLogFile(string path)
        {
            if (!m_ReadingLogs)
                return;

            this.Dispatcher.Invoke(System.Windows.Threading.DispatcherPriority.Normal, (Action)(() =>
            {
                // Show progress
                this.Title = path;
            }));

            try
            {
                System.IO.DirectoryInfo di = new System.IO.DirectoryInfo(path);  // Dir to search from
                string filter = "*.log";  // Search filter

                // Get the files in this directory...
                foreach (System.IO.FileInfo fi in di.GetFiles(filter))
                {
                    this.Dispatcher.Invoke(System.Windows.Threading.DispatcherPriority.Normal, (Action)(() =>
                    {
                        // Add the log if it hasn't already been added
                        if (!IsInList(listBoxLogs.Items, fi.FullName))
                        {
                            if (CheckLogIsACPLog(fi.FullName))
                            {
                                listBoxLogs.Items.Add(fi.FullName);
                                UpdateLogListCount(listBoxLogs.Items.Count);
                            }
                            else
                            {
                                // Silently reject non-ACP logs
                                m_nonACPLogFileCount++;
                            }
                        }
                    }));
                    
                    // Are we being stopped?
                    if (!m_ReadingLogs)
                        return;
                }

                // Call ourselves recursively to get files in subdirs...
                foreach (System.IO.DirectoryInfo subDir in di.GetDirectories())
                {
                    if (!m_ReadingLogs)
                        return;

                    ReadLogFile(subDir.FullName);
                }
            }
            catch
            {
                // Ignore
            }
        }

        private void ReadLogsComplete(IAsyncResult result)
        {
            this.Dispatcher.Invoke(System.Windows.Threading.DispatcherPriority.Normal, (Action)(() =>
            {
                ResetUI();
                this.Title = this.Title = GetMainWndTitle();

                if (m_nonACPLogFileCount > 0)
                    MessageBox.Show(m_nonACPLogFileCount.ToString() +
                        " files were not added because they are not valid ACP logs", "File(s) not added");
            }));
        }

        private void ParseLogsComplete(IAsyncResult result)
        {
            this.Dispatcher.Invoke(System.Windows.Threading.DispatcherPriority.Normal, (Action)(() =>
            {
                ResetParsingAllUI();
            }));
        }
        #endregion

        #region Graph plotting
        private void PreparePlot(GraphType graphType, string title, string logPath)
        {
            switch (graphType)
            {
                case GraphType.NoSelection:
                    return;

                case GraphType.FilterChangeTime:
                case GraphType.FWHM:
                case GraphType.FWHMvHFD:
                case GraphType.HFD:
                case GraphType.GuiderSettleTime:
                case GraphType.GuiderStartUpTime:
                case GraphType.PointErrorCntr:
                case GraphType.PointErrorObj:
                case GraphType.PtExpSolveTime:
                case GraphType.SlewTimeTgts:
                case GraphType.AFTimes:
                    if (string.IsNullOrEmpty(logPath))
                    {
                        m_plottingLogs = false;
                        return;  // Can't plot properties on a single log as none is selected
                    }
                    else
                    {
                        // Kick-off the parsing process on a new thread...
                        m_parseLogsOp = new ParseLogs(DoParseLogs);
                        m_parseLogsOp.BeginInvoke(logPath, true, PreparePlotsComplete, null);
                        break;
                    }

                case GraphType.FilterChangeTimeAllLogs:
                case GraphType.FWHMAvgAllLogs:
                case GraphType.FWHMvHFDAllLogs:
                case GraphType.HFDAvgAllLogs:
                case GraphType.GuiderSettleTimeLogAllLogs:
                case GraphType.GuiderStartUpTimeAllLogs:
                case GraphType.PointErrorCntrAllLogs:
                case GraphType.PointErrorObjAllLogs:
                case GraphType.PtExpSolveTimeAllLogs:
                case GraphType.SlewTimeTgtsAllLogs:
                case GraphType.AFTimesAllLogs:
                    // Kick-off the parsing process on a new thread...
                    m_parseLogsOp = new ParseLogs(DoParseLogs); 
                    m_parseLogsOp.BeginInvoke(null /* null => parse all logs */, true, PreparePlotsComplete, null);
                    break;
            }
        }

        private void PreparePlotsComplete(IAsyncResult result)
        {
            // The log(s) to plot will be in m_logs
            ParsingLogs = false;

            this.Dispatcher.Invoke(System.Windows.Threading.DispatcherPriority.Normal, (Action)(() =>
            {
                Plot();
            }));
        }

        private void Plot()
        {
            if (m_report == null || m_report.Logs == null || m_report.Logs.Count == 0)
                return;

            // Create the plot data and graph...
            m_grDataSeries1 = new List<KeyValuePair<object, object>>();
            m_grDataSeries2 = new List<KeyValuePair<object, object>>();
            m_grDataSeries3 = new List<KeyValuePair<object, object>>();

            Graph graph = new Graph(this, null);
            graph.Title = m_graphTitle;  // Windows title
            graph.DataSourceSeries1 = m_grDataSeries1;
            graph.DataSourceSeries2 = m_grDataSeries2;
            graph.DataSourceSeries3 = m_grDataSeries3;

            try
            {
                switch (m_graphType)
                {
                    case GraphType.FilterChangeTime:
                        graph.graphUC.xAxis.Title = "\nTime";
                        graph.graphUC.yAxis.Title = "Filter Change Time (secs)";
                        graph.graphUC.lineSeries.Title = "Filter Change Time"; // Legend/key
                        graph.graphUC.lineSeries2.Title = "Average"; // Legend/key
                        graph.graphUC.GraphTitle.Text = m_graphTitle;
                        PlotLog(graph, m_report.Logs[0], LogEventType.FilterChange, false, true, LogEventType.None);
                        break;

                    case GraphType.FilterChangeTimeAllLogs:
                        graph.graphUC.xAxis.Title = "\nDate";
                        graph.graphUC.yAxis.Title = "Average Filter Change Time (secs)";
                        graph.graphUC.lineSeries.Title = "Avg Filter Change Time";
                        graph.graphUC.lineSeries2.Title = "Overall Average";
                        graph.graphUC.GraphTitle.Text = m_graphTitle;
                        foreach (Log lg in m_report.Logs)
                            PlotLog(graph, lg, LogEventType.FilterChange, true, false, LogEventType.None);
                        CalcOverallAverage();
                        break;

                    case GraphType.FWHM:
                        graph.graphUC.xAxis.Title = "\nTime";
                        graph.graphUC.yAxis.Title = "FWHM";
                        graph.graphUC.lineSeries.Title = "FWHM";
                        graph.graphUC.lineSeries2.Title = "Average";
                        graph.graphUC.GraphTitle.Text = m_graphTitle;
                        PlotLog(graph, m_report.Logs[0], LogEventType.FWHM, false, true, LogEventType.None);
                        break;

                    case GraphType.FWHMAvgAllLogs:
                        graph.graphUC.xAxis.Title = "\nDate";
                        graph.graphUC.yAxis.Title = "Average FWHM";
                        graph.graphUC.lineSeries.Title = "Avg FWHM";
                        graph.graphUC.lineSeries2.Title = "Overall Avg FWHM";
                        graph.graphUC.GraphTitle.Text = m_graphTitle;
                        foreach (Log lg in m_report.Logs)
                            PlotLog(graph, lg, LogEventType.FWHM, true, false, LogEventType.None);
                        CalcOverallAverage();
                        break;

                    case GraphType.FWHMvHFD:
                        graph.graphUC.xAxis.Title = "\nFWHM Time";
                        graph.graphUC.yAxis.Title = "FWHM\\HFD";
                        graph.graphUC.xAxisTertiary.Title = "HFD Time\n";
                        graph.graphUC.lineSeries.Title = "FWHM";
                        graph.graphUC.lineSeries2.Title = "Average FWHM";
                        graph.graphUC.lineSeries3.Title = "HFD";
                        graph.graphUC.GraphTitle.Text = m_graphTitle;
                        PlotLog(graph, m_report.Logs[0], LogEventType.FWHM, false, true, LogEventType.HFD);
                        break;

                    case GraphType.FWHMvHFDAllLogs:
                        graph.graphUC.xAxis.Title = "\nFWHM Date";
                        graph.graphUC.yAxis.Title = "Average FWHM\\HFD";
                        graph.graphUC.xAxisTertiary.Title = "HFD Date\n";
                        graph.graphUC.lineSeries.Title = "Avg FWHM";
                        graph.graphUC.lineSeries2.Title = "Overall Avg FWHM";
                        graph.graphUC.lineSeries3.Title = "Avg HFD";
                        graph.graphUC.GraphTitle.Text = m_graphTitle;
                        foreach (Log lg in m_report.Logs)
                            PlotLog(graph, lg, LogEventType.FWHM, true, false, LogEventType.HFD);
                        CalcOverallAverage();
                        break;

                    case GraphType.HFD:
                        graph.graphUC.xAxis.Title = "\nTime";
                        graph.graphUC.yAxis.Title = "HFD";
                        graph.graphUC.lineSeries.Title = "HFD";
                        graph.graphUC.lineSeries2.Title = "Average HFD";
                        graph.graphUC.GraphTitle.Text = m_graphTitle;
                        PlotLog(graph, m_report.Logs[0], LogEventType.HFD, false, true, LogEventType.None);
                        break;

                    case GraphType.HFDAvgAllLogs:
                        graph.graphUC.xAxis.Title = "\nDate";
                        graph.graphUC.yAxis.Title = "Average HFD";
                        graph.graphUC.lineSeries.Title = "Avg HFD";
                        graph.graphUC.lineSeries2.Title = "Overall Average HFD";
                        graph.graphUC.GraphTitle.Text = m_graphTitle;
                        foreach (Log lg in m_report.Logs)
                            PlotLog(graph, lg, LogEventType.HFD, true, false, LogEventType.None);
                        CalcOverallAverage();
                        break;

                    case GraphType.GuiderSettleTime:
                        graph.graphUC.xAxis.Title = "\nTime";
                        graph.graphUC.yAxis.Title = "Guider Settle Time (secs)";
                        graph.graphUC.lineSeries.Title = "Guider Settle Time";
                        graph.graphUC.lineSeries2.Title = "Average";
                        graph.graphUC.GraphTitle.Text = m_graphTitle;
                        PlotLog(graph, m_report.Logs[0], LogEventType.GuiderSettle, false, true, LogEventType.None);
                        break;

                    case GraphType.GuiderSettleTimeLogAllLogs:
                        graph.graphUC.xAxis.Title = "\nDate";
                        graph.graphUC.yAxis.Title = "Average Guider Settle Time (secs)";
                        graph.graphUC.lineSeries.Title = "Avg Guider Settle Time";
                        graph.graphUC.lineSeries2.Title = "Overall Average";
                        foreach (Log lg in m_report.Logs)
                            PlotLog(graph, lg, LogEventType.GuiderSettle, true, false, LogEventType.None);
                        CalcOverallAverage();
                        break;

                    case GraphType.GuiderStartUpTime:
                        graph.graphUC.xAxis.Title = "\nTime";
                        graph.graphUC.yAxis.Title = "Guider Start-Up Time (secs)";
                        graph.graphUC.lineSeries.Title = "Guider Start-Up Time";
                        graph.graphUC.lineSeries2.Title = "Average";
                        graph.graphUC.GraphTitle.Text = m_graphTitle;
                        PlotLog(graph, m_report.Logs[0], LogEventType.GuiderStartUp, false, true, LogEventType.None);
                        break;

                    case GraphType.GuiderStartUpTimeAllLogs:
                        graph.graphUC.xAxis.Title = "\nDate";
                        graph.graphUC.yAxis.Title = "Average Guider Start-Up Time (secs)";
                        graph.graphUC.lineSeries.Title = "Avg Guider Start-Up Time";
                        graph.graphUC.lineSeries2.Title = "Overall Average";
                        graph.graphUC.GraphTitle.Text = m_graphTitle;
                        foreach (Log lg in m_report.Logs)
                            PlotLog(graph, lg, LogEventType.GuiderStartUp, true, false, LogEventType.None);
                        CalcOverallAverage();
                        break;

                    case GraphType.PointErrorCntr:
                        graph.graphUC.xAxis.Title = "\nTime";
                        graph.graphUC.yAxis.Title = "Pointing Error (center slew)(arcmins)";
                        graph.graphUC.lineSeries.Title = "Pointing Error";
                        graph.graphUC.lineSeries2.Title = "Average";
                        graph.graphUC.GraphTitle.Text = m_graphTitle;
                        PlotLog(graph, m_report.Logs[0], LogEventType.PointingErrorCenterSlew, false, true, LogEventType.None);
                        break;

                    case GraphType.PointErrorCntrAllLogs:
                        graph.graphUC.xAxis.Title = "\nDate";
                        graph.graphUC.yAxis.Title = "Average Pointing Error (center slew)(arcmins)";
                        graph.graphUC.lineSeries.Title = "Avg Pointing Error";
                        graph.graphUC.lineSeries2.Title = "Overall Average";
                        graph.graphUC.GraphTitle.Text = m_graphTitle;
                        foreach (Log lg in m_report.Logs)
                            PlotLog(graph, lg, LogEventType.PointingErrorCenterSlew, true, false, LogEventType.None);
                        CalcOverallAverage();
                        break;

                    case GraphType.PointErrorObj:
                        graph.graphUC.xAxis.Title = "\nTime";
                        graph.graphUC.yAxis.Title = "Pointing Error (object slew)(arcmins)";
                        graph.graphUC.lineSeries.Title = "Pointing Error";
                        graph.graphUC.lineSeries2.Title = "Average";
                        graph.graphUC.GraphTitle.Text = m_graphTitle;
                        PlotLog(graph, m_report.Logs[0], LogEventType.PointingErrorObjectSlew, false, true, LogEventType.None);
                        break;

                    case GraphType.PointErrorObjAllLogs:
                        graph.graphUC.xAxis.Title = "\nDate";
                        graph.graphUC.yAxis.Title = "Average Pointing Error (object slew)(arcmins)";
                        graph.graphUC.lineSeries.Title = "Avg Pointing Error";
                        graph.graphUC.lineSeries2.Title = "Overall Average";
                        graph.graphUC.GraphTitle.Text = m_graphTitle;
                        foreach (Log lg in m_report.Logs)
                            PlotLog(graph, lg, LogEventType.PointingErrorObjectSlew, true, false, LogEventType.None);
                        CalcOverallAverage();
                        break;

                    case GraphType.PtExpSolveTime:
                        graph.graphUC.xAxis.Title = "\nTime";
                        graph.graphUC.yAxis.Title = "Pointing Exp/Plate Solve Time (secs)";
                        graph.graphUC.lineSeries.Title = "Pointing Exp/Plate Solve Time";
                        graph.graphUC.lineSeries2.Title = "Average";
                        graph.graphUC.GraphTitle.Text = m_graphTitle;
                        PlotLog(graph, m_report.Logs[0], LogEventType.PointingExpAndPlateSolve, false, true, LogEventType.None);
                        break;

                    case GraphType.PtExpSolveTimeAllLogs:
                        graph.graphUC.xAxis.Title = "\nDate";
                        graph.graphUC.yAxis.Title = "Average Pointing Exp/Plate Solve Time (secs)";
                        graph.graphUC.lineSeries.Title = "Avg Pnt Exp/Plate Solve Time";
                        graph.graphUC.lineSeries2.Title = "Overall Average";
                        graph.graphUC.GraphTitle.Text = m_graphTitle;
                        foreach (Log lg in m_report.Logs)
                            PlotLog(graph, lg, LogEventType.PointingExpAndPlateSolve, true, false, LogEventType.None);
                        CalcOverallAverage();
                        break;

                    case GraphType.SlewTimeTgts:
                        graph.graphUC.xAxis.Title = "\nTime";
                        graph.graphUC.yAxis.Title = "Slew Time (targets)(secs)";
                        graph.graphUC.lineSeries.Title = "Slew Time";
                        graph.graphUC.lineSeries2.Title = "Average";
                        graph.graphUC.GraphTitle.Text = m_graphTitle;
                        PlotLog(graph, m_report.Logs[0], LogEventType.SlewTarget, false, true, LogEventType.None);
                        break;

                    case GraphType.SlewTimeTgtsAllLogs:
                        graph.graphUC.xAxis.Title = "\nDate";
                        graph.graphUC.yAxis.Title = "Average Slew Time (targets)(secs)";
                        graph.graphUC.lineSeries.Title = "Average Slew Time";
                        graph.graphUC.lineSeries2.Title = "Overall Average";
                        graph.graphUC.GraphTitle.Text = m_graphTitle;
                        foreach (Log lg in m_report.Logs)
                            PlotLog(graph, lg, LogEventType.SlewTarget, true, false, LogEventType.None);
                        CalcOverallAverage();
                        break;

                    case GraphType.AFTimes:
                        graph.graphUC.xAxis.Title = "\nTime";
                        graph.graphUC.yAxis.Title = "Auto-Focus Time (secs)";
                        graph.graphUC.lineSeries.Title = "AF Time";
                        graph.graphUC.lineSeries2.Title = "Average";
                        graph.graphUC.GraphTitle.Text = m_graphTitle;
                        PlotLog(graph, m_report.Logs[0], LogEventType.AutoFocus, false, true, LogEventType.None);
                        break;

                    case GraphType.AFTimesAllLogs:
                        graph.graphUC.xAxis.Title = "\nDate";
                        graph.graphUC.yAxis.Title = "Average Auto-Focus Time (secs)";
                        graph.graphUC.lineSeries.Title = "Average Auto-Focus Time";
                        graph.graphUC.lineSeries2.Title = "Overall Average";
                        graph.graphUC.GraphTitle.Text = m_graphTitle;
                        foreach (Log lg in m_report.Logs)
                            PlotLog(graph, lg, LogEventType.AutoFocus, true, false, LogEventType.None);
                        CalcOverallAverage();
                        break;
                }
            }
            catch
            {
                MessageBox.Show("Data error while plotting graph", "Graph Error");
                m_plottingLogs = false;
                return;
            }

            graph.Show();
            m_plottingLogs = false;
        }

        private void PlotLog(Graph graph, Log log, LogEventType let, bool showDate, bool showAllEvents, LogEventType letTertiary)
        {
            double tmpAvg = 0;
            int tmpCount = 0;

            graph.graphUC.LogEventTypeMainSeries = let;
            graph.graphUC.LogEventTypeTertiarySeries = letTertiary;
            graph.Log = log;

            if (log == null || m_grDataSeries1 == null || m_grDataSeries2 == null || graph == null)
                return;

            #region Plot all events for a single log
            if (showAllEvents)  // showAllEvents == true means plot each event in the log, otherwise just the average for the log
            {
                if (letTertiary == LogEventType.None)
                    graph.graphUC.ShowThirdSeries(false);

                foreach (LogEvent le in log.LogEvents)
                {
                    if (le.EventType == let && le.StartDate != null && le.Data != null)
                    {
                        // Plot all instances of the event...
                        if (showDate)
                            m_grDataSeries1.Add(new KeyValuePair<object, object>(le.StartDate.Value.ToShortDateString(), le.Data));
                        else
                            m_grDataSeries1.Add(new KeyValuePair<object, object>(le.StartDate.Value.ToLongTimeString(), le.Data));  // Time

                        tmpAvg += (double)le.Data;
                        tmpCount++;

                        graph.graphUC.RefreshData();  // This will add the new plot point 
                    }
                    else if(le.EventType == letTertiary && le.StartDate != null && le.Data != null)
                    {
                        if (showDate)
                            m_grDataSeries3.Add(new KeyValuePair<object, object>(le.StartDate.Value.ToShortDateString(), le.Data));
                        else
                            m_grDataSeries3.Add(new KeyValuePair<object, object>(le.StartDate.Value.ToLongTimeString(), le.Data));

                        graph.graphUC.RefreshData();
                    }
                }

                tmpAvg = (double)(tmpAvg / (double)tmpCount);
                tmpAvg = Math.Round(tmpAvg, 2);
                foreach (KeyValuePair<object, object> kvp in m_grDataSeries1)
                    m_grDataSeries2.Add(new KeyValuePair<object, object>(kvp.Key, tmpAvg));  // Add the average (i.e. we have a series with the same number of datapoints as the main series, but all with the same (avg) value)
            }
            #endregion

            #region Plot a single average value for the log
            else
            {
                // Just plot the single average value for all the events of the specified type in the log 
                double avgSeries1 = 0;
                double avgCountSeries1 = 0;
                double avgSeries3 = 0;
                double avgCountSeries3 = 0;
                foreach (LogEvent le in log.LogEvents)
                {
                    if (le.EventType == let && le.StartDate != null && le.Data != null)
                    {
                        avgCountSeries1++;
                        avgSeries1 += (double)le.Data;
                    }
                    else if (le.EventType == letTertiary && letTertiary != LogEventType.None && le.StartDate != null && le.Data != null)
                    {
                        avgCountSeries3++;
                        avgSeries3 += (double)le.Data;
                    }
                }

                if (avgSeries1 > 0 && avgCountSeries1 > 0)
                {
                    avgSeries1 = (double)(avgSeries1 / (double)avgCountSeries1);
                    avgSeries1 = Math.Round(avgSeries1, 2);
                }
                else
                    avgSeries1 = 0;

                if (avgSeries1 > 0)
                {
                    if (showDate)
                        m_grDataSeries1.Add(new KeyValuePair<object, object>(log.StartDate.Value.ToShortDateString(), avgSeries1));
                    else
                        m_grDataSeries1.Add(new KeyValuePair<object, object>(log.StartDate.Value.ToLongTimeString(), avgSeries1));  // Time

                    graph.graphUC.RefreshData();  // This will add the new plot point 
                }

                if (letTertiary != LogEventType.None)
                {
                    if (avgSeries3 > 0 && avgCountSeries3 > 0)
                    {
                        avgSeries3 = (double)(avgSeries3 / (double)avgCountSeries3);
                        avgSeries3 = Math.Round(avgSeries3, 2);
                    }
                    else
                        avgSeries3 = 0;

                    if (avgSeries3 > 0)
                    {
                        if (showDate)
                            m_grDataSeries3.Add(new KeyValuePair<object, object>(log.StartDate.Value.ToShortDateString(), avgSeries3));
                        else
                            m_grDataSeries3.Add(new KeyValuePair<object, object>(log.StartDate.Value.ToLongTimeString(), avgSeries3));  // Time

                        graph.graphUC.RefreshData();
                    }
                }
                else
                    graph.graphUC.ShowThirdSeries(false);
            }
            #endregion
        }

        private void CalcOverallAverage()
        {
            // Overall avg (series 2)...
            double tmpAvg = 0;
            int tmpCount = 0;

            // First, calc the avg of the main series
            foreach (KeyValuePair<object, object> kvp in m_grDataSeries1)
            {
                tmpAvg += (double)kvp.Value;
                tmpCount++;
            }

            tmpAvg = (double)(tmpAvg / (double)tmpCount);
            tmpAvg = Math.Round(tmpAvg, 2);

            // Now create the (series 2) datapoints...
            foreach (KeyValuePair<object, object> kvp in m_grDataSeries1)
                m_grDataSeries2.Add(new KeyValuePair<object, object>(kvp.Key, tmpAvg));  // Add the average
        }
        #endregion
    }
}
